{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "a930wM_fqUNH"
      },
      "source": [
        "##### Copyright 2021 The TensorFlow Federated Authors."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "id": "VxVUPYkahDa6"
      },
      "outputs": [],
      "source": [
        "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n",
        "# you may not use this file except in compliance with the License.\n",
        "# You may obtain a copy of the License at\n",
        "#\n",
        "# https://www.apache.org/licenses/LICENSE-2.0\n",
        "#\n",
        "# Unless required by applicable law or agreed to in writing, software\n",
        "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
        "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
        "# See the License for the specific language governing permissions and\n",
        "# limitations under the License."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "naLrzWx6FbQQ"
      },
      "source": [
        "# Client-efficient large-model federated learning via `federated_select` and sparse aggregation\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4jM4S9YFXamd"
      },
      "source": [
        "\u003ctable class=\"tfo-notebook-buttons\" align=\"left\"\u003e\n",
        "  \u003ctd\u003e\n",
        "    \u003ca target=\"_blank\" href=\"https://www.tensorflow.org/federated/tutorials/sparse_federated_learning\"\u003e\u003cimg src=\"https://www.tensorflow.org/images/tf_logo_32px.png\" /\u003eView on TensorFlow.org\u003c/a\u003e\n",
        "  \u003c/td\u003e\n",
        "  \u003ctd\u003e\n",
        "    \u003ca target=\"_blank\" href=\"https://colab.research.google.com/github/tensorflow/federated/blob/v0.84.0/docs/tutorials/sparse_federated_learning.ipynb\"\u003e\u003cimg src=\"https://www.tensorflow.org/images/colab_logo_32px.png\" /\u003eRun in Google Colab\u003c/a\u003e\n",
        "  \u003c/td\u003e\n",
        "  \u003ctd\u003e\n",
        "    \u003ca target=\"_blank\" href=\"https://github.com/tensorflow/federated/blob/v0.84.0/docs/tutorials/sparse_federated_learning.ipynb\"\u003e\u003cimg src=\"https://www.tensorflow.org/images/GitHub-Mark-32px.png\" /\u003eView source on GitHub\u003c/a\u003e\n",
        "  \u003c/td\u003e\n",
        "  \u003ctd\u003e\n",
        "    \u003ca href=\"https://storage.googleapis.com/tensorflow_docs/federated/docs/tutorials/sparse_federated_learning.ipynb\"\u003e\u003cimg src=\"https://www.tensorflow.org/images/download_logo_32px.png\" /\u003eDownload notebook\u003c/a\u003e\n",
        "  \u003c/td\u003e\n",
        "\u003c/table\u003e"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "saL2ruE25Cx1"
      },
      "source": [
        "This tutorial shows how TFF can be used to train a very large model where each client device only downloads and updates a small part of the model, using\n",
        "`tff.federated_select` and sparse aggregation. While this tutorial is fairly self-contained, the [`tff.federated_select` tutorial](https://www.tensorflow.org/federated/tutorials/federated_select) and [custom FL algorithms tutorial](https://www.tensorflow.org/federated/tutorials/building_your_own_federated_learning_algorithm) provide good introductions to some of the techniques used here.\n",
        "\n",
        "Concretely, in this tutorial we consider logistic regression for multi-label classification, predicting which \"tags\" are associated with a text string based on a  bag-of-words feature representation. Importantly, communication and client-side computation costs are controlled by a fixed constant (`MAX_TOKENS_SELECTED_PER_CLIENT`), and *do not* scale with the overall vocabulary size, which could be extremely large in practical settings."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Fg3OmMVWp_9D"
      },
      "outputs": [],
      "source": [
        "#@test {\"skip\": true}\n",
        "!pip install --quiet --upgrade tensorflow-federated"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "89APlnIQCvmp"
      },
      "outputs": [],
      "source": [
        "import collections\n",
        "from collections.abc import Callable\n",
        "import itertools\n",
        "\n",
        "import numpy as np\n",
        "import tensorflow as tf\n",
        "import tensorflow_federated as tff"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "92wW8RDBvE9F"
      },
      "source": [
        "Each client will `federated_select` the rows of the model weights for at most this many unique tokens. This upper-bounds the size of the client's local model and the amount of server -\u003e client (`federated_select`) and client - \u003e server `(federated_aggregate`) communication performed.\n",
        "\n",
        "This tutorial should still run correctly even if you set this as small as 1 (ensuring not all tokens from each client are selected) or to a large value, though model convergence may be effected."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "dkDumr_6bDtY"
      },
      "outputs": [],
      "source": [
        "MAX_TOKENS_SELECTED_PER_CLIENT = 6"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Th2PI1FpvaSL"
      },
      "source": [
        "We also define a few constants for various types. For this colab, a **token** is an integer identifier for a particular word after parsing the dataset. "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "zyQ9xmIyvZFO"
      },
      "outputs": [],
      "source": [
        "# There are some constraints on types\n",
        "# here that will require some explicit type conversions:\n",
        "#    - `tff.federated_select` requires int32\n",
        "#    - `tf.SparseTensor` requires int64 indices.\n",
        "TOKEN_DTYPE = np.int64\n",
        "SELECT_KEY_DTYPE = np.int32\n",
        "\n",
        "# Type for counts of token occurences.\n",
        "TOKEN_COUNT_DTYPE = np.int32\n",
        "\n",
        "# A sparse feature vector can be thought of as a map\n",
        "# from TOKEN_DTYPE to FEATURE_DTYPE. \n",
        "# Our features are {0, 1} indicators, so we could potentially\n",
        "# use np.int8 as an optimization.\n",
        "FEATURE_DTYPE = np.int32"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "2ej2eY4-zk_a"
      },
      "source": [
        "# Setting up the problem: Dataset and Model\n",
        "\n",
        "We construct a tiny toy dataset for easy experimentation in this tutorial. However, the format of the dataset is compatible with [Federated StackOverflow](https://www.tensorflow.org/federated/api_docs/python/tff/simulation/datasets/stackoverflow/load_data), and \n",
        "the [pre-processing](https://github.com/google-research/federated/blob/0a558bac8a724fc38175ff4f0ce46c7af3d24be2/utils/datasets/stackoverflow_tag_prediction.py) and [model architecture](https://github.com/google-research/federated/blob/49a43456aa5eaee3e1749855eed89c0087983541/utils/models/stackoverflow_lr_models.py) are adopted from the StackOverflow \n",
        "tag prediction problem of [*Adaptive Federated Optimization*](https://arxiv.org/abs/2003.00295)."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "J5ofeMf1phYY"
      },
      "source": [
        "## Dataset parsing and pre-processing"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "WM9Fae0PXSfa"
      },
      "outputs": [],
      "source": [
        "NUM_OOV_BUCKETS = 1\n",
        "\n",
        "BatchType = collections.namedtuple('BatchType', ['tokens', 'tags'])\n",
        "\n",
        "def build_to_ids_fn(word_vocab: list[str],\n",
        "                    tag_vocab: list[str]) -\u003e Callable[[tf.Tensor], tf.Tensor]:\n",
        "  \"\"\"Constructs a function mapping examples to sequences of token indices.\"\"\"\n",
        "  word_table_values = np.arange(len(word_vocab), dtype=np.int64)\n",
        "  word_table = tf.lookup.StaticVocabularyTable(\n",
        "      tf.lookup.KeyValueTensorInitializer(word_vocab, word_table_values),\n",
        "      num_oov_buckets=NUM_OOV_BUCKETS)\n",
        "\n",
        "  tag_table_values = np.arange(len(tag_vocab), dtype=np.int64)\n",
        "  tag_table = tf.lookup.StaticVocabularyTable(\n",
        "      tf.lookup.KeyValueTensorInitializer(tag_vocab, tag_table_values),\n",
        "      num_oov_buckets=NUM_OOV_BUCKETS)\n",
        "\n",
        "  def to_ids(example):\n",
        "    \"\"\"Converts a Stack Overflow example to a bag-of-words/tags format.\"\"\"\n",
        "    sentence = tf.strings.join([example['tokens'], example['title']],\n",
        "                               separator=' ')\n",
        "\n",
        "    # We represent that label (output tags) densely.\n",
        "    raw_tags = example['tags']\n",
        "    tags = tf.strings.split(raw_tags, sep='|')\n",
        "    tags = tag_table.lookup(tags)\n",
        "    tags, _ = tf.unique(tags)\n",
        "    tags = tf.one_hot(tags, len(tag_vocab) + NUM_OOV_BUCKETS)\n",
        "    tags = tf.reduce_max(tags, axis=0)\n",
        "\n",
        "    # We represent the features as a SparseTensor of {0, 1}s.\n",
        "    words = tf.strings.split(sentence)\n",
        "    tokens = word_table.lookup(words)\n",
        "    tokens, _ = tf.unique(tokens)\n",
        "    # Note:  We could choose to use the word counts as the feature vector\n",
        "    # instead of just {0, 1} values (see tf.unique_with_counts).\n",
        "    tokens = tf.reshape(tokens, shape=(tf.size(tokens), 1))\n",
        "    tokens_st = tf.SparseTensor(\n",
        "        tokens,\n",
        "        tf.ones(tf.size(tokens), dtype=FEATURE_DTYPE),\n",
        "        dense_shape=(len(word_vocab) + NUM_OOV_BUCKETS,))\n",
        "    tokens_st = tf.sparse.reorder(tokens_st)\n",
        "\n",
        "    return BatchType(tokens_st, tags)\n",
        "\n",
        "  return to_ids"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Gv9NMKgbYki9"
      },
      "outputs": [],
      "source": [
        "def build_preprocess_fn(word_vocab, tag_vocab):\n",
        "\n",
        "  @tf.function\n",
        "  def preprocess_fn(dataset):\n",
        "    to_ids = build_to_ids_fn(word_vocab, tag_vocab)\n",
        "    # We *don't* shuffle in order to make this colab deterministic for\n",
        "    # easier testing and reproducibility.\n",
        "    # But real-world training should use `.shuffle()`.\n",
        "    return dataset.map(to_ids, num_parallel_calls=tf.data.experimental.AUTOTUNE)\n",
        "\n",
        "  return preprocess_fn"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Jxji1x30tZCi"
      },
      "source": [
        "## A tiny toy dataset\n",
        "We construct a tiny toy dataset with a global vocabulary of 12 words and 3 clients. This tiny example is useful for testing edge cases (for example,\n",
        "we have two clients with less than `MAX_TOKENS_SELECTED_PER_CLIENT = 6` distinct tokens, and one with more) and developing the code.\n",
        " \n",
        "However, the real-world use cases of this approach would be global vocabularies of 10s of millions or more, with perhaps 1000s of distinct tokens appearing on each client. Because the format of the data is the same, the extension to more realistic testbed problems, e.g. the `tff.simulation.datasets.stackoverflow.load_data()` dataset, should be straightforward.\n",
        "\n",
        "First, we define our word and tag vocabularies."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9gtatbx4tdPo"
      },
      "outputs": [],
      "source": [
        "# Features\n",
        "FRUIT_WORDS = ['apple', 'orange', 'pear', 'kiwi']\n",
        "VEGETABLE_WORDS = ['carrot', 'broccoli', 'arugula', 'peas']\n",
        "FISH_WORDS = ['trout', 'tuna', 'cod', 'salmon']\n",
        "WORD_VOCAB = FRUIT_WORDS + VEGETABLE_WORDS + FISH_WORDS\n",
        "\n",
        "# Labels\n",
        "TAG_VOCAB = ['FRUIT', 'VEGETABLE', 'FISH']"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "PQLblngP7gmJ"
      },
      "source": [
        "Now, we create 3 clients with small local datasets. If you are running this tutorial in colab, it may be useful to use the \"mirror cell in tab\" feature to pin this cell and its output in order to interpret/check the output of the functions developed below."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Vr74BLPM1Mxa"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Word vocab\n",
            " 0 apple\n",
            " 1 orange\n",
            " 2 pear\n",
            " 3 kiwi\n",
            " 4 carrot\n",
            " 5 broccoli\n",
            " 6 arugula\n",
            " 7 peas\n",
            " 8 trout\n",
            " 9 tuna\n",
            "10 cod\n",
            "11 salmon\n",
            "\n",
            "Tag vocab\n",
            " 0 FRUIT\n",
            " 1 VEGETABLE\n",
            " 2 FISH\n"
          ]
        }
      ],
      "source": [
        "preprocess_fn = build_preprocess_fn(WORD_VOCAB, TAG_VOCAB)\n",
        "\n",
        "\n",
        "def make_dataset(raw):\n",
        "  d = tf.data.Dataset.from_tensor_slices(\n",
        "      # Matches the StackOverflow formatting\n",
        "      collections.OrderedDict(\n",
        "          tokens=tf.constant([t[0] for t in raw]),\n",
        "          tags=tf.constant([t[1] for t in raw]),\n",
        "          title=['' for _ in raw]))\n",
        "  d = preprocess_fn(d)\n",
        "  return d\n",
        "\n",
        "\n",
        "# 4 distinct tokens\n",
        "CLIENT1_DATASET = make_dataset([\n",
        "    ('apple orange apple orange', 'FRUIT'),\n",
        "    ('carrot trout', 'VEGETABLE|FISH'),\n",
        "    ('orange apple', 'FRUIT'),\n",
        "    ('orange', 'ORANGE|CITRUS')  # 2 OOV tag\n",
        "])\n",
        "\n",
        "# 6 distinct tokens\n",
        "CLIENT2_DATASET = make_dataset([\n",
        "    ('pear cod', 'FRUIT|FISH'),\n",
        "    ('arugula peas', 'VEGETABLE'),\n",
        "    ('kiwi pear', 'FRUIT'),\n",
        "    ('sturgeon', 'FISH'),  # OOV word\n",
        "    ('sturgeon bass', 'FISH')  # 2 OOV words\n",
        "])\n",
        "\n",
        "# A client with all possible words \u0026 tags (13 distinct tokens).\n",
        "# With MAX_TOKENS_SELECTED_PER_CLIENT = 6, we won't download the model\n",
        "# slices for all tokens that occur on this client.\n",
        "CLIENT3_DATASET = make_dataset([\n",
        "    (' '.join(WORD_VOCAB + ['oovword']), '|'.join(TAG_VOCAB)),\n",
        "    # Mathe the OOV token and 'salmon' occur in the largest number\n",
        "    # of examples on this client:\n",
        "    ('salmon oovword', 'FISH|OOVTAG')\n",
        "])\n",
        "\n",
        "print('Word vocab')\n",
        "for i, word in enumerate(WORD_VOCAB):\n",
        "  print(f'{i:2d} {word}')\n",
        "\n",
        "print('\\nTag vocab')\n",
        "for i, tag in enumerate(TAG_VOCAB):\n",
        "  print(f'{i:2d} {tag}')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "HH3T6M0f8Bh2"
      },
      "source": [
        "Define constants for the raw numbers of input features (tokens/words) and labels (post tags). Our actual input/output spaces are `NUM_OOV_BUCKETS = 1` larger because we add an OOV token / tag."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "vBULpAepxloO"
      },
      "outputs": [],
      "source": [
        "NUM_WORDS = len(WORD_VOCAB) \n",
        "NUM_TAGS = len(TAG_VOCAB)\n",
        "\n",
        "WORD_VOCAB_SIZE = NUM_WORDS + NUM_OOV_BUCKETS\n",
        "TAG_VOCAB_SIZE = NUM_TAGS + NUM_OOV_BUCKETS"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "7zF6NF_f8PQR"
      },
      "source": [
        "Create batched versions of the datasets, and individual batches, which will be useful in testing code as we go."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "4uxRQAYdJISP"
      },
      "outputs": [],
      "source": [
        "batched_dataset1 = CLIENT1_DATASET.batch(2)\n",
        "batched_dataset2 = CLIENT2_DATASET.batch(3)\n",
        "batched_dataset3 = CLIENT3_DATASET.batch(2)\n",
        "\n",
        "batch1 = next(iter(batched_dataset1))\n",
        "batch2 = next(iter(batched_dataset2))\n",
        "batch3 = next(iter(batched_dataset3))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "DBcObjtlaq8q"
      },
      "source": [
        "## Define a model with sparse inputs\n",
        "\n",
        "We use a simple independent logistic regression model for each tag."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "RTtIkxCKao2Y"
      },
      "outputs": [],
      "source": [
        "def create_logistic_model(word_vocab_size: int, vocab_tags_size: int):\n",
        "\n",
        "  model = tf.keras.models.Sequential([\n",
        "      tf.keras.layers.InputLayer(input_shape=(word_vocab_size,), sparse=True),\n",
        "      tf.keras.layers.Dense(\n",
        "          vocab_tags_size,\n",
        "          activation='sigmoid',\n",
        "          kernel_initializer=tf.keras.initializers.zeros,\n",
        "          # For simplicity, don't use a bias vector; this means the model\n",
        "          # is a single tensor, and we only need sparse aggregation of\n",
        "          # the per-token slices of the model. Generalizing to also handle\n",
        "          # other model weights that are fully updated \n",
        "          # (non-dense broadcast and aggregate) would be a good exercise.\n",
        "          use_bias=False),\n",
        "  ])\n",
        "\n",
        "  return model"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "DCMScjuWn9t-"
      },
      "source": [
        "Let's make sure it works, first by making predictions:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "cGQOnvbfa3w4"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[[0.5 0.5 0.5 0.5]\n",
            " [0.5 0.5 0.5 0.5]]\n"
          ]
        }
      ],
      "source": [
        "model = create_logistic_model(WORD_VOCAB_SIZE, TAG_VOCAB_SIZE)\n",
        "p = model.predict(batch1.tokens)\n",
        "print(p)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "AOT2veElT-ij"
      },
      "source": [
        "And some simple centralized training:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "jn3lB84WgRgV"
      },
      "outputs": [],
      "source": [
        "model.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.001),\n",
        "              loss=tf.keras.losses.BinaryCrossentropy())\n",
        "model.train_on_batch(batch1.tokens, batch1.tags)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "z1aahpVomC4G"
      },
      "source": [
        "# Building blocks for the federated computation\n",
        " \n",
        "We will implement a simple version of the [Federated Averaging](https://arxiv.org/abs/1602.05629) algorithm with the key difference that each device only downloads a relevant subset of the model, and only contributes updates to that subset.\n",
        " \n",
        "We use `M` as shorthand for `MAX_TOKENS_SELECTED_PER_CLIENT`. At a high level, one round of training involves these steps:\n",
        "\n",
        "  1. Each participating client scans over its local dataset, parsing the input strings and mapping them to the correct tokens (int indexes). This requires access to the global (large) dictionary (this could potentially be avoided using [feature hashing](https://en.wikipedia.org/wiki/Feature_hashing) techniques). We then sparsely count how many times each token occurs. If `U` unique tokens occur on device, we choose the `num_actual_tokens = min(U, M)` most frequent tokens to train.\n",
        "\n",
        "  1. The clients use `federated_select` to retrieve the model coefficients for the `num_actual_tokens` selected tokens from the server. Each model slice is a tensor of shape `(TAG_VOCAB_SIZE, )`, so the total data transmitted to the client is at most of size `TAG_VOCAB_SIZE * M` (see note below).\n",
        "\n",
        "  1. The clients construct a mapping `global_token -\u003e local_token` where the  local token (int index) is the index of the global token in the list of selected tokens.\n",
        " \n",
        "  1. The clients use a \"small\" version of the global model that only has coefficients for at most `M` tokens, from the range `[0, num_actual_tokens)`. The `global -\u003e local` mapping is used to initialize the dense parameters of this model from the selected model slices.\n",
        " \n",
        "  1. Clients train their local model using SGD on data preprocessed with the `global -\u003e local` mapping.\n",
        " \n",
        "  1. Clients turn the parameters of their local model into `IndexedSlices` updates using the `local -\u003e global` mapping to index the rows. The server aggregates these updates using a sparse sum aggregation.\n",
        " \n",
        "  1. The server takes the (dense) result of the above aggregation, divides it by the number of clients participating, and applies the resulting average update to the global model.\n",
        " \n",
        "In this section we construct the building blocks for these steps, which will then be combined in a final `federated_computation` that captures the full logic of one training round.\n",
        " \n",
        "\u003e NOTE: The above description hides one technical detail: Both `federated_select` and the construction of the local model require statically known shapes, and so we cannot use the dynamic per-client `num_actual_tokens` size. Instead, we use the static value `M`, adding padding where needed. This does not impact that semantics of the algorithm."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "yrB3RWrCYHd_"
      },
      "source": [
        "### Count client tokens and decide which model slices to `federated_select`\n",
        "\n",
        "Each device needs to decide which \"slices\" of the model are relevant to its local training dataset.  For our problem, we do this by (sparsely!) counting how many examples contain each token in the client training data set.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "GRr0ip0ja31O"
      },
      "outputs": [],
      "source": [
        "@tf.function\n",
        "def token_count_fn(token_counts, batch):\n",
        "  \"\"\"Adds counts from `batch` to the running `token_counts` sum.\"\"\"\n",
        "  # Sum across the batch dimension.\n",
        "  flat_tokens = tf.sparse.reduce_sum(\n",
        "      batch.tokens, axis=0, output_is_sparse=True)\n",
        "  flat_tokens = tf.cast(flat_tokens, dtype=TOKEN_COUNT_DTYPE)\n",
        "  return tf.sparse.add(token_counts, flat_tokens)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "LTZdZZhFgeYz"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "tokens: [0 1 4 8]\n",
            "counts: [2 3 1 1]\n"
          ]
        }
      ],
      "source": [
        "# Simple tests\n",
        "# Create the initial zero token counts using empty tensors.\n",
        "initial_token_counts = tf.SparseTensor(\n",
        "    indices=tf.zeros(shape=(0, 1), dtype=TOKEN_DTYPE),\n",
        "    values=tf.zeros(shape=(0,), dtype=TOKEN_COUNT_DTYPE),\n",
        "    dense_shape=(WORD_VOCAB_SIZE,))\n",
        "\n",
        "client_token_counts = batched_dataset1.reduce(initial_token_counts,\n",
        "                                              token_count_fn)\n",
        "tokens = tf.reshape(client_token_counts.indices, (-1,)).numpy()\n",
        "print('tokens:', tokens)\n",
        "np.testing.assert_array_equal(tokens, [0, 1, 4, 8])\n",
        "# The count is the number of *examples* in which the token/word\n",
        "# occurs, not the total number of occurences, since we still featurize\n",
        "# multiple occurences in the same example as a \"1\".\n",
        "counts = client_token_counts.values.numpy()\n",
        "print('counts:', counts)\n",
        "np.testing.assert_array_equal(counts, [2, 3, 1, 1])"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "n5ghfTtZgce7"
      },
      "source": [
        "We will select the model parameters corresponding to the `MAX_TOKENS_SELECTED_PER_CLIENT` most frequently occuring tokens on device. If\n",
        "fewer than this many tokens occur on device, we pad the list to enable the use\n",
        "of `federated_select`.\n",
        " \n",
        "Note that other strategies are possibly better, for example, randomly selecting tokens (perhaps based on their occurrence probability). This would ensure that all slices of the model (for which the client has data) have some chance of being updated."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "45YOfL8fh5K8"
      },
      "outputs": [],
      "source": [
        "@tf.function\n",
        "def keys_for_client(client_dataset, max_tokens_per_client):\n",
        "  \"\"\"Computes a set of max_tokens_per_client keys.\"\"\"\n",
        "  initial_token_counts = tf.SparseTensor(\n",
        "      indices=tf.zeros((0, 1), dtype=TOKEN_DTYPE),\n",
        "      values=tf.zeros((0,), dtype=TOKEN_COUNT_DTYPE),\n",
        "      dense_shape=(WORD_VOCAB_SIZE,))\n",
        "  client_token_counts = client_dataset.reduce(initial_token_counts,\n",
        "                                              token_count_fn)\n",
        "  # Find the most-frequently occuring tokens\n",
        "  tokens = tf.reshape(client_token_counts.indices, shape=(-1,))\n",
        "  counts = client_token_counts.values\n",
        "  perm = tf.argsort(counts, direction='DESCENDING')\n",
        "  tokens = tf.gather(tokens, perm)\n",
        "  counts = tf.gather(counts, perm)\n",
        "  num_raw_tokens = tf.shape(tokens)[0]\n",
        "  actual_num_tokens = tf.minimum(max_tokens_per_client, num_raw_tokens)\n",
        "  selected_tokens = tokens[:actual_num_tokens]\n",
        "  paddings = [[0, max_tokens_per_client - tf.shape(selected_tokens)[0]]]\n",
        "  padded_tokens = tf.pad(selected_tokens, paddings=paddings)\n",
        "  # Make sure the type is statically determined\n",
        "  padded_tokens = tf.reshape(padded_tokens, shape=(max_tokens_per_client,))\n",
        "\n",
        "  # We will pass these tokens as keys into `federated_select`, which\n",
        "  # requires SELECT_KEY_DTYPE=np.int32 keys.\n",
        "  padded_tokens = tf.cast(padded_tokens, dtype=SELECT_KEY_DTYPE)\n",
        "  return padded_tokens, actual_num_tokens"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "bTyJVDqkjGyG"
      },
      "outputs": [],
      "source": [
        "# Simple test\n",
        "\n",
        "# Case 1: actual_num_tokens \u003e max_tokens_per_client\n",
        "selected_tokens, actual_num_tokens = keys_for_client(batched_dataset1, 3)\n",
        "assert tf.size(selected_tokens) == 3\n",
        "assert actual_num_tokens == 3\n",
        "\n",
        "# Case 2: actual_num_tokens \u003c max_tokens_per_client\n",
        "selected_tokens, actual_num_tokens = keys_for_client(batched_dataset1, 10)\n",
        "assert tf.size(selected_tokens) == 10\n",
        "assert actual_num_tokens == 4"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "GO0pJ8D-dzp7"
      },
      "source": [
        "### Map global tokens to local tokens\n",
        "The above selection gives us a dense set of tokens in the range `[0, actual_num_tokens)` which we will use for the on-device model. However, the dataset we read has tokens from the much larger global vocabulary range `[0, WORD_VOCAB_SIZE)`. \n",
        "\n",
        "Thus, we need to map the global tokens to their corresponding local tokens. The\n",
        "local token ids are simply given by the indexes into the `selected_tokens` tensor computed in the previous step."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "UG3uLlqJohXg"
      },
      "outputs": [],
      "source": [
        "@tf.function\n",
        "def map_to_local_token_ids(client_data, client_keys):\n",
        "  global_to_local = tf.lookup.StaticHashTable(\n",
        "      # Note int32 -\u003e int64 maps are not supported\n",
        "      tf.lookup.KeyValueTensorInitializer(\n",
        "          keys=tf.cast(client_keys, dtype=TOKEN_DTYPE),\n",
        "          # Note we need to use tf.shape, not the static \n",
        "          # shape client_keys.shape[0]\n",
        "          values=tf.range(0, limit=tf.shape(client_keys)[0],\n",
        "                          dtype=TOKEN_DTYPE)),\n",
        "      # We use -1 for tokens that were not selected, which can occur for clients\n",
        "      # with more than MAX_TOKENS_SELECTED_PER_CLIENT distinct tokens.\n",
        "      # We will simply remove these invalid indices from the batch below.\n",
        "      default_value=-1)\n",
        "\n",
        "  def to_local_ids(sparse_tokens):\n",
        "    indices_t = tf.transpose(sparse_tokens.indices)\n",
        "    batch_indices = indices_t[0]  # First column\n",
        "    tokens = indices_t[1]  # Second column\n",
        "    tokens = tf.map_fn(\n",
        "        lambda global_token_id: global_to_local.lookup(global_token_id), tokens)\n",
        "    # Remove tokens that aren't actually available (looked up as -1):\n",
        "    available_tokens = tokens \u003e= 0\n",
        "    tokens = tokens[available_tokens]\n",
        "    batch_indices = batch_indices[available_tokens]\n",
        "\n",
        "    updated_indices = tf.transpose(\n",
        "        tf.concat([[batch_indices], [tokens]], axis=0))\n",
        "    st = tf.sparse.SparseTensor(\n",
        "        updated_indices,\n",
        "        tf.ones(tf.size(tokens), dtype=FEATURE_DTYPE),\n",
        "        # Each client has at most MAX_TOKENS_SELECTED_PER_CLIENT distinct tokens.\n",
        "        dense_shape=[sparse_tokens.dense_shape[0], MAX_TOKENS_SELECTED_PER_CLIENT])\n",
        "    st = tf.sparse.reorder(st)\n",
        "    return st\n",
        "\n",
        "  return client_data.map(lambda b: BatchType(to_local_ids(b.tokens), b.tags))"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "y_Cn60NIBUWf"
      },
      "outputs": [],
      "source": [
        "# Simple test\n",
        "client_keys, actual_num_tokens = keys_for_client(\n",
        "    batched_dataset3, MAX_TOKENS_SELECTED_PER_CLIENT)\n",
        "client_keys = client_keys[:actual_num_tokens]\n",
        "\n",
        "d = map_to_local_token_ids(batched_dataset3, client_keys)\n",
        "batch  = next(iter(d))\n",
        "all_tokens = tf.gather(batch.tokens.indices, indices=1, axis=1)\n",
        "# Confirm we have local indices in the range [0, MAX):\n",
        "assert tf.math.reduce_max(all_tokens) \u003c MAX_TOKENS_SELECTED_PER_CLIENT\n",
        "assert tf.math.reduce_max(all_tokens) \u003e= 0"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4nvZYfzga1yN"
      },
      "source": [
        "### Train the local (sub)model on each client\n",
        "\n",
        "Note `federated_select` will return the selected slices as a `tf.data.Dataset` in the same order as the selection keys. So, we first define a utility function to take such a Dataset and convert it to a single dense tensor which can be used as the model weights of the client model."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "gFpC_nFjgbtI"
      },
      "outputs": [],
      "source": [
        "@tf.function\n",
        "def slices_dataset_to_tensor(slices_dataset):\n",
        "  \"\"\"Convert a dataset of slices to a tensor.\"\"\"\n",
        "  # Use batching to gather all of the slices into a single tensor.\n",
        "  d = slices_dataset.batch(MAX_TOKENS_SELECTED_PER_CLIENT,\n",
        "                           drop_remainder=False)\n",
        "  iter_d = iter(d)\n",
        "  tensor = next(iter_d)\n",
        "  # Make sure we have consumed everything\n",
        "  opt = iter_d.get_next_as_optional()\n",
        "  tf.Assert(tf.logical_not(opt.has_value()), data=[''], name='CHECK_EMPTY')\n",
        "  return tensor"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "xX0gtz5Ylvqf"
      },
      "outputs": [],
      "source": [
        "# Simple test\n",
        "weights = np.random.random(\n",
        "    size=(MAX_TOKENS_SELECTED_PER_CLIENT, TAG_VOCAB_SIZE)).astype(np.float32)\n",
        "model_slices_as_dataset = tf.data.Dataset.from_tensor_slices(weights)\n",
        "weights2 = slices_dataset_to_tensor(model_slices_as_dataset)\n",
        "np.testing.assert_array_equal(weights, weights2)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ft0ROTYl0laV"
      },
      "source": [
        "We now have all the components we need to define a simple local training loop which will run on each client."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "aG66d8UR_9Gm"
      },
      "outputs": [],
      "source": [
        "@tf.function\n",
        "def client_train_fn(model, client_optimizer,\n",
        "                    model_slices_as_dataset, client_data,\n",
        "                    client_keys, actual_num_tokens):\n",
        "  \n",
        "  initial_model_weights = slices_dataset_to_tensor(model_slices_as_dataset)\n",
        "  assert len(model.trainable_variables) == 1\n",
        "  model.trainable_variables[0].assign(initial_model_weights)\n",
        "\n",
        "  # Only keep the \"real\" (unpadded) keys.\n",
        "  client_keys = client_keys[:actual_num_tokens]\n",
        " \n",
        "  client_data = map_to_local_token_ids(client_data, client_keys)\n",
        "\n",
        "  loss_fn = tf.keras.losses.BinaryCrossentropy()\n",
        "  for features, labels in client_data:\n",
        "    with tf.GradientTape() as tape:\n",
        "      predictions = model(features)\n",
        "      loss = loss_fn(labels, predictions)\n",
        "    grads = tape.gradient(loss, model.trainable_variables)\n",
        "    client_optimizer.apply_gradients(zip(grads, model.trainable_variables))\n",
        "\n",
        "  model_weights_delta = model.trainable_weights[0] - initial_model_weights\n",
        "  model_weights_delta = tf.slice(model_weights_delta, begin=[0, 0], \n",
        "                           size=[actual_num_tokens, -1])\n",
        "  return client_keys, model_weights_delta"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "3XOe0thb2TQr"
      },
      "outputs": [],
      "source": [
        "# Simple test\n",
        "# Note if you execute this cell a second time, you need to also re-execute\n",
        "# the preceeding cell to avoid \"tf.function-decorated function tried to \n",
        "# create variables on non-first call\" errors.\n",
        "on_device_model = create_logistic_model(MAX_TOKENS_SELECTED_PER_CLIENT,\n",
        "                                        TAG_VOCAB_SIZE)\n",
        "client_optimizer = tf.keras.optimizers.SGD(learning_rate=0.001)\n",
        "client_keys, actual_num_tokens = keys_for_client(\n",
        "    batched_dataset2, MAX_TOKENS_SELECTED_PER_CLIENT)\n",
        "\n",
        "model_slices_as_dataset = tf.data.Dataset.from_tensor_slices(\n",
        "    np.zeros((MAX_TOKENS_SELECTED_PER_CLIENT, TAG_VOCAB_SIZE),\n",
        "             dtype=np.float32))\n",
        "\n",
        "keys, delta = client_train_fn(\n",
        "    on_device_model,\n",
        "    client_optimizer,\n",
        "    model_slices_as_dataset,\n",
        "    client_data=batched_dataset3,\n",
        "    client_keys=client_keys,\n",
        "    actual_num_tokens=actual_num_tokens)\n",
        "\n",
        "print(delta)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "xwJFTBczKSYH"
      },
      "source": [
        "###  Aggregate IndexedSlices\n",
        "We use `tff.federated_aggregate` to construct a federated sparse sum for `IndexedSlices`. This simple implementation has the constraint that the\n",
        "`dense_shape` is known statically in advance. Note also that this sum is only *semi-sparse*, in the sense that the client -\u003e server communication is sparse, but the server maintains a dense representation of the sum in `accumulate` and `merge`, and outputs this dense representation.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "EIG_XYd2OYeO"
      },
      "outputs": [],
      "source": [
        "def federated_indexed_slices_sum(slice_indices, slice_values, dense_shape):\n",
        "  \"\"\"\n",
        "  Sums IndexedSlices@CLIENTS to a dense @SERVER Tensor.\n",
        "\n",
        "  Intermediate aggregation is performed by converting to a dense representation,\n",
        "  which may not be suitable for all applications.\n",
        "\n",
        "  Args:\n",
        "    slice_indices: An IndexedSlices.indices tensor @CLIENTS.\n",
        "    slice_values: An IndexedSlices.values tensor @CLIENTS.\n",
        "    dense_shape: A statically known dense shape.\n",
        "\n",
        "  Returns:\n",
        "    A dense tensor placed @SERVER representing the sum of the client's\n",
        "    IndexedSclies.\n",
        "  \"\"\"\n",
        "  slices_dtype = slice_values.type_signature.member.dtype\n",
        "  zero = tff.tensorflow.computation(\n",
        "      lambda: tf.zeros(dense_shape, dtype=slices_dtype))()\n",
        "\n",
        "  @tf.function\n",
        "  def accumulate_slices(dense, client_value):\n",
        "    indices, slices = client_value\n",
        "    # There is no built-in way to add `IndexedSlices`, but \n",
        "    # tf.convert_to_tensor is a quick way to convert to a dense representation\n",
        "    # so we can add them.\n",
        "    return dense + tf.convert_to_tensor(\n",
        "        tf.IndexedSlices(slices, indices, dense_shape))\n",
        "\n",
        "\n",
        "  return tff.federated_aggregate(\n",
        "      (slice_indices, slice_values),\n",
        "      zero=zero,\n",
        "      accumulate=tff.tensorflow.computation(accumulate_slices),\n",
        "      merge=tff.tensorflow.computation(lambda d1, d2: tf.add(d1, d2, name='merge')),\n",
        "      report=tff.tensorflow.computation(lambda d: d))\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "769qwlLhKFA9"
      },
      "source": [
        "Construct a minimal `federated_computation` as a test"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "i_FIvwj3UIAq"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "({\u003cint64[?],float32[?,2]\u003e}@CLIENTS -\u003e float32[6,2]@SERVER)\n"
          ]
        }
      ],
      "source": [
        "dense_shape = (6, 2)\n",
        "indices_type = tff.TensorType(np.int64, (None,))\n",
        "values_type = tff.TensorType(np.float32, (None, 2))\n",
        "client_slice_type = tff.FederatedType(\n",
        "    (indices_type, values_type), tff.CLIENTS)\n",
        "\n",
        "@tff.federated_computation(client_slice_type)\n",
        "def test_sum_indexed_slices(indices_values_at_client):\n",
        "  indices, values = indices_values_at_client\n",
        "  return federated_indexed_slices_sum(indices, values, dense_shape)\n",
        "\n",
        "print(test_sum_indexed_slices.type_signature)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "dulcCZg2S_9f"
      },
      "outputs": [],
      "source": [
        "x = tf.IndexedSlices(\n",
        "    values=np.array([[2., 2.1], [0., 0.1], [1., 1.1], [5., 5.1]],\n",
        "                    dtype=np.float32),\n",
        "    indices=[2, 0, 1, 5],\n",
        "    dense_shape=dense_shape)\n",
        "y = tf.IndexedSlices(\n",
        "    values=np.array([[0., 0.3], [3.1, 3.2]], dtype=np.float32),\n",
        "    indices=[1, 3],\n",
        "    dense_shape=dense_shape)\n",
        "\n",
        "# Sum one.\n",
        "result = test_sum_indexed_slices([(x.indices, x.values)])\n",
        "np.testing.assert_array_equal(tf.convert_to_tensor(x), result)\n",
        "\n",
        "# Sum two.\n",
        "expected = [[0., 0.1], [1., 1.4], [2., 2.1], [3.1, 3.2], [0., 0.], [5., 5.1]]\n",
        "result = test_sum_indexed_slices([(x.indices, x.values), (y.indices, y.values)])\n",
        "np.testing.assert_array_almost_equal(expected, result)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "2sXg8fDQhwB7"
      },
      "source": [
        "# Putting it all together in a `federated_computation`\n",
        "We now use TFF to bind together the components into a `tff.federated_computation`.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9yRpGJr_i3XK"
      },
      "outputs": [],
      "source": [
        "DENSE_MODEL_SHAPE = (WORD_VOCAB_SIZE, TAG_VOCAB_SIZE)\n",
        "client_data_type = tff.SequenceType(batched_dataset1.element_spec)\n",
        "model_type = tff.TensorType(np.float32, shape=DENSE_MODEL_SHAPE)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ITawWBbodTdE"
      },
      "source": [
        "We use a basic server training function based on Federated Averaging, applying the update with a server learning rate of 1.0. It is important that we apply an update (delta) to the model, rather than simply averaging client-supplied models, as otherwise if a given slice of the model wasn't trained on by any client on a given round its coefficients could be zeroed out."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "gGgKTpnscdZI"
      },
      "outputs": [],
      "source": [
        "@tff.tensorflow.computation\n",
        "def server_update(current_model_weights, update_sum, num_clients):\n",
        "  average_update = update_sum / num_clients\n",
        "  return current_model_weights + average_update"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bSFAmgzqBRHA"
      },
      "source": [
        "We need a couple more `tff.tensorflow.computation` components:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "soFQAmktUtYp"
      },
      "outputs": [],
      "source": [
        "# Function to select slices from the model weights in federated_select:\n",
        "select_fn = tff.tensorflow.computation(\n",
        "    lambda model_weights, index: tf.gather(model_weights, index))\n",
        "\n",
        "\n",
        "# We need to wrap `client_train_fn` as a `tff.tensorflow.computation`, making\n",
        "# sure we do any operations that might construct `tf.Variable`s outside\n",
        "# of the `tf.function` we are wrapping.\n",
        "@tff.tensorflow.computation\n",
        "def client_train_fn_tff(model_slices_as_dataset, client_data, client_keys,\n",
        "                        actual_num_tokens):\n",
        "  # Note this is amaller than the global model, using\n",
        "  # MAX_TOKENS_SELECTED_PER_CLIENT which is much smaller than WORD_VOCAB_SIZE.\n",
        "  # We would like a model of size `actual_num_tokens`, but we\n",
        "  # can't build the model dynamically, so we will slice off the padded\n",
        "  # weights at the end.\n",
        "  client_model = create_logistic_model(MAX_TOKENS_SELECTED_PER_CLIENT,\n",
        "                                       TAG_VOCAB_SIZE)\n",
        "  client_optimizer = tf.keras.optimizers.SGD(learning_rate=0.1)\n",
        "  return client_train_fn(client_model, client_optimizer,\n",
        "                         model_slices_as_dataset, client_data, client_keys,\n",
        "                         actual_num_tokens)\n",
        "\n",
        "@tff.tensorflow.computation\n",
        "def keys_for_client_tff(client_data):\n",
        "  return keys_for_client(client_data, MAX_TOKENS_SELECTED_PER_CLIENT)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ftI2fCGNBJQ-"
      },
      "source": [
        "We're now ready to put all the pieces together!"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "LM_2mzkgBOjl"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "(\u003cserver_model=float32[13,4]@SERVER,client_data={\u003ctokens=\u003cindices=int64[?,2],values=int32[?],dense_shape=int64[2]\u003e,tags=float32[?,4]\u003e*}@CLIENTS\u003e -\u003e float32[13,4]@SERVER)\n"
          ]
        }
      ],
      "source": [
        "@tff.federated_computation(\n",
        "    tff.FederatedType(model_type, tff.SERVER), tff.FederatedType(client_data_type, tff.CLIENTS))\n",
        "def sparse_model_update(server_model, client_data):\n",
        "  max_tokens = tff.federated_value(MAX_TOKENS_SELECTED_PER_CLIENT, tff.SERVER)\n",
        "  keys_at_clients, actual_num_tokens = tff.federated_map(\n",
        "      keys_for_client_tff, client_data)\n",
        "\n",
        "  model_slices = tff.federated_select(keys_at_clients, max_tokens, server_model,\n",
        "                                      select_fn)\n",
        "\n",
        "  update_keys, update_slices = tff.federated_map(\n",
        "      client_train_fn_tff,\n",
        "      (model_slices, client_data, keys_at_clients, actual_num_tokens))\n",
        "\n",
        "  dense_update_sum = federated_indexed_slices_sum(update_keys, update_slices,\n",
        "                                                  DENSE_MODEL_SHAPE)\n",
        "  num_clients = tff.federated_sum(tff.federated_value(1.0, tff.CLIENTS))\n",
        "\n",
        "  updated_server_model = tff.federated_map(\n",
        "      server_update, (server_model, dense_update_sum, num_clients))\n",
        "\n",
        "  return updated_server_model\n",
        "\n",
        "\n",
        "print(sparse_model_update.type_signature)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "OSvveGwdZFCO"
      },
      "source": [
        "# Let's train a model!\n",
        "\n",
        "Now that we have our training function, let's try it out."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "Rn_V-E3FdrLO"
      },
      "outputs": [],
      "source": [
        "server_model = create_logistic_model(WORD_VOCAB_SIZE, TAG_VOCAB_SIZE)\n",
        "server_model.compile(  # Compile to make evaluation easy.\n",
        "    optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.0),  # Unused\n",
        "    loss=tf.keras.losses.BinaryCrossentropy(),\n",
        "    metrics=[ \n",
        "      tf.keras.metrics.Precision(name='precision'),\n",
        "      tf.keras.metrics.AUC(name='auc'),\n",
        "      tf.keras.metrics.Recall(top_k=2, name='recall_at_2'),\n",
        "  ])\n",
        "\n",
        "def evaluate(model, dataset, name):\n",
        "  metrics = model.evaluate(dataset, verbose=0)\n",
        "  metrics_str = ', '.join([f'{k}={v:.2f}' for k, v in \n",
        "                          (zip(server_model.metrics_names, metrics))])\n",
        "  print(f'{name}: {metrics_str}')"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "bw7S9PmfZ3Bw"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Before training\n",
            "Client 1: loss=0.69, precision=0.00, auc=0.50, recall_at_2=0.60\n",
            "Client 2: loss=0.69, precision=0.00, auc=0.50, recall_at_2=0.50\n",
            "Client 3: loss=0.69, precision=0.00, auc=0.50, recall_at_2=0.40\n",
            "Training on clients [0 1]\n",
            "Training on clients [0 2 1]\n",
            "Training on clients [2 0]\n",
            "Training on clients [1 0 2]\n",
            "Training on clients [2]\n",
            "Training on clients [2 0]\n",
            "Training on clients [1 2 0]\n",
            "Training on clients [0]\n",
            "Training on clients [2]\n",
            "Training on clients [1 2]\n",
            "After training\n",
            "Client 1: loss=0.67, precision=0.80, auc=0.91, recall_at_2=0.80\n",
            "Client 2: loss=0.68, precision=0.67, auc=0.96, recall_at_2=1.00\n",
            "Client 3: loss=0.65, precision=1.00, auc=0.93, recall_at_2=0.80\n"
          ]
        }
      ],
      "source": [
        "print('Before training')\n",
        "evaluate(server_model, batched_dataset1, 'Client 1')\n",
        "evaluate(server_model, batched_dataset2, 'Client 2')\n",
        "evaluate(server_model, batched_dataset3, 'Client 3')\n",
        "\n",
        "model_weights = server_model.trainable_weights[0]\n",
        "\n",
        "client_datasets = [batched_dataset1, batched_dataset2, batched_dataset3]\n",
        "for _ in range(10):  # Run 10 rounds of FedAvg\n",
        "  # We train on 1, 2, or 3 clients per round, selecting\n",
        "  # randomly.\n",
        "  cohort_size = np.random.randint(1, 4)\n",
        "  clients = np.random.choice([0, 1, 2], cohort_size, replace=False)\n",
        "  print('Training on clients', clients)\n",
        "  model_weights = sparse_model_update(\n",
        "      model_weights, [client_datasets[i] for i in clients])\n",
        "server_model.set_weights([model_weights])\n",
        "\n",
        "print('After training')\n",
        "evaluate(server_model, batched_dataset1, 'Client 1')\n",
        "evaluate(server_model, batched_dataset2, 'Client 2')\n",
        "evaluate(server_model, batched_dataset3, 'Client 3')"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "collapsed_sections": [],
      "name": "sparse_federated_learning.ipynb",
      "toc_visible": true
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
